/* eslint no-console: 0 */
const path = require('path');
const makeLoaderFinder = require('razzle-dev-utils/makeLoaderFinder');
const nodeExternals = require('webpack-node-externals');
const LoadablePlugin = require('@loadable/webpack-plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const fs = require('fs');
const RootResolverPlugin = require('./webpack-plugins/webpack-root-resolver');
const RelativeResolverPlugin = require('./webpack-plugins/webpack-relative-resolver');
const createAddonsLoader = require('./create-addons-loader');
const AddonConfigurationRegistry = require('./addon-registry');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

const fileLoaderFinder = makeLoaderFinder('file-loader');

const projectRootPath = path.resolve('.');
const languages = require('./src/constants/Languages');
const { poToJson } = require('@plone/scripts/i18n.cjs');

const packageJson = require(path.join(projectRootPath, 'package.json'));

const registry = new AddonConfigurationRegistry(projectRootPath);

const defaultModify = ({
  env: { target, dev },
  webpackConfig: config,
  webpackObject: webpack,
  options,
}) => {
  // Compile language JSON files from po files
  poToJson({ registry, addonMode: false });

  if (dev) {
    config.plugins.unshift(
      new webpack.DefinePlugin({
        __DEVELOPMENT__: true,
      }),
    );
  } else {
    config.plugins.unshift(
      new webpack.DefinePlugin({
        __DEVELOPMENT__: false,
      }),
    );
  }

  if (target === 'web') {
    config.plugins.unshift(
      new webpack.DefinePlugin({
        __CLIENT__: true,
        __SERVER__: false,
      }),
    );

    config.plugins.push(
      new LoadablePlugin({
        outputAsset: false,
        writeToDisk: { filename: path.resolve(`${projectRootPath}/build`) },
      }),
    );

    if (dev && process.env.DEBUG_CIRCULAR) {
      config.plugins.push(
        new CircularDependencyPlugin({
          exclude: /node_modules/,
          // `onStart` is called before the cycle detection starts
          onStart({ compilation }) {
            console.log('start detecting webpack modules cycles');
          },
          failOnError: false,
          // `onDetected` is called for each module that is cyclical
          onDetected({ module: webpackModuleRecord, paths, compilation }) {
            // `paths` will be an Array of the relative module paths that make up the cycle
            // `module` will be the module record generated by webpack that caused the cycle
            compilation.warnings.push(new Error(paths.join(' -> ')));
          },
          // `onEnd` is called before the cycle detection ends
          onEnd({ compilation }) {
            console.log(
              `Detected ${compilation.warnings.length} circular dependencies`,
            );
            compilation.warnings.forEach((item) => {
              if (item.message.includes('config')) {
                console.log(item.message);
              }
            });
          },
        }),
      );
    }

    config.output.filename = dev
      ? 'static/js/[name].js'
      : 'static/js/[name].[chunkhash:8].js';

    config.optimization = Object.assign({}, config.optimization, {
      runtimeChunk: true,
      splitChunks: {
        chunks: 'all',
        name: dev,
        cacheGroups: {
          // We reset the default values set by webpack
          // So the chunks have all proper names (no random numbers)
          // The CSS gets bundled in one CSS chunk and it's consistent with
          // the `style-loader` load order, so no difference between
          // local (project CSS) and `node_modules` ones.
          vendors: false,
          default: false,
        },
      },
    });

    // This is needed to override Razzle use of the unmaintained CleanCSS
    // which does not have support for recently CSS features (container queries).
    // Using the default provided (cssnano) by css-minimizer-webpack-plugin
    // should be enough see:
    // (https://github.com/clean-css/clean-css/discussions/1209)
    if (!dev) {
      config.optimization = Object.assign({}, config.optimization, {
        minimizer: [
          new TerserPlugin(options.webpackOptions.terserPluginOptions),
          new CssMinimizerPlugin({
            sourceMap: options.razzleOptions.enableSourceMaps,
            minimizerOptions: {
              sourceMap: options.razzleOptions.enableSourceMaps,
            },
          }),
        ],
      });
    }

    config.plugins.unshift(
      // restrict moment.js locales to en/de
      // see https://github.com/jmblog/how-to-optimize-momentjs-with-webpack for details
      new webpack.ContextReplacementPlugin(
        /moment[/\\]locale$/,
        new RegExp(Object.keys(languages).join('|')),
      ),
      new LodashModuleReplacementPlugin({
        shorthands: true,
        cloning: true,
        currying: true,
        caching: true,
        collections: true,
        exotics: true,
        guards: true,
        metadata: true,
        deburring: true,
        unicode: true,
        chaining: true,
        memoizing: true,
        coercions: true,
        flattening: true,
        paths: true,
        placeholders: true,
      }),
    );
  }

  if (target === 'node') {
    config.plugins.unshift(
      new webpack.DefinePlugin({
        __CLIENT__: false,
        __SERVER__: true,
      }),
    );

    // Razzle sets some of its basic env vars in the default config injecting them (for
    // the client use, mainly) in a `DefinePlugin` instance. However, this also ends in
    // the server build, removing the ability of the server node process to read from
    // the system's (or process') env vars. In this case, in the server build, we hunt
    // down the instance of the `DefinePlugin` defined by Razzle and remove the
    // `process.env.PORT` so it can be overridable in runtime
    const idxArr = config.plugins
      .map((plugin, idx) =>
        plugin.constructor.name === 'DefinePlugin' ? idx : '',
      )
      .filter(String);
    idxArr.forEach((index) => {
      const { definitions } = config.plugins[index];
      if (definitions['process.env.PORT']) {
        const newDefs = Object.assign({}, definitions);
        // Transforms the stock RAZZLE_PUBLIC_DIR into relative path,
        // so one can move the build around
        newDefs['process.env.RAZZLE_PUBLIC_DIR'] = newDefs[
          'process.env.RAZZLE_PUBLIC_DIR'
        ].replace(projectRootPath, '.');
        // Handles the PORT, so it takes the real PORT from the runtime enviroment var,
        // but keeps the one from build time, if different from 3000 (by not deleting it)
        // So build time one takes precedence, do not set it in build time if you want
        // to control it always via runtime (assumming 3000 === not set in build time)
        if (newDefs['process.env.PORT'] === '3000') {
          delete newDefs['process.env.PORT'];
        }
        config.plugins[index] = new webpack.DefinePlugin(newDefs);
      }
    });
  }

  // Don't load config|variables|overrides) files with file-loader
  // Don't load SVGs from ./src/icons with file-loader
  const fileLoader = config.module.rules.find(fileLoaderFinder);
  fileLoader.exclude = [
    /\.(config|variables|overrides)$/,
    /icons\/.*\.svg$/,
    ...fileLoader.exclude,
  ];

  let addonsFromEnvVar = [];
  if (process.env.ADDONS) {
    addonsFromEnvVar = process.env.ADDONS.split(';');
  }

  const addonsLoaderPath = createAddonsLoader(
    registry.getAddonDependencies(),
    registry.getAddons(),
  );

  config.resolve.plugins = [
    new RelativeResolverPlugin(registry),
    new RootResolverPlugin(),
  ];

  config.resolve.alias = {
    ...registry.getAddonCustomizationPaths(),
    ...registry.getAddonsFromEnvVarCustomizationPaths(),
    ...registry.getProjectCustomizationPaths(),
    ...config.resolve.alias,
    '../../theme.config$': `${projectRootPath}/theme/theme.config`,
    'volto-themes': `${registry.voltoPath}/theme/themes`,
    'load-volto-addons': addonsLoaderPath,
    ...registry.getResolveAliases(),
    '@plone/volto': `${registry.voltoPath}/src`,
    // to be able to reference path uncustomized by webpack
    '@plone/volto-original': `${registry.voltoPath}/src`,
    // be able to reference current package from customized package
    '@package': `${projectRootPath}/src`,
    '@root': `${projectRootPath}/src`,
    // we're incorporating redux-connect
    'redux-connect': `${registry.voltoPath}/src/helpers/AsyncConnect`,
    // avoids including lodash multiple times.
    // semantic-ui-react uses lodash-es, everything else uses lodash
    'lodash-es': path.dirname(require.resolve('lodash')),
  };

  config.performance = {
    maxAssetSize: 10000000,
    maxEntrypointSize: 10000000,
  };

  let addonsAsExternals = [];

  const { include } = options.webpackOptions.babelRule;
  if (packageJson.name !== '@plone/volto') {
    include.push(fs.realpathSync(`${registry.voltoPath}/src`));
  }

  // Add babel support external (ie. node_modules npm published packages)
  const packagesNames = Object.keys(registry.packages);
  if (registry.packages && packagesNames.length > 0) {
    packagesNames.forEach((addon) => {
      const p = fs.realpathSync(registry.packages[addon].modulePath);
      if (include.indexOf(p) === -1) {
        include.push(p);
      }
    });
    addonsAsExternals = packagesNames.map((addon) => new RegExp(addon));
  }

  if (process.env.ADDONS) {
    addonsFromEnvVar.forEach((addon) => {
      const normalizedAddonName = addon.split(':')[0];
      const p = fs.realpathSync(
        registry.packages[normalizedAddonName].modulePath,
      );
      if (include.indexOf(p) === -1) {
        include.push(p);
      }
      addonsAsExternals = [
        ...addonsAsExternals,
        ...packagesNames.map(
          (normalizedAddonName) => new RegExp(normalizedAddonName),
        ),
      ];
    });
  }

  // write a .dot file with the graph
  // convert it to svg with: `dot addon-dependency-graph.dot -Tsvg -o out.svg`
  if (process.env.DEBUG_ADDONS_LOADER && target === 'node') {
    const addonsDepGraphPath = path.join(
      process.cwd(),
      'addon-dependency-graph.dot',
    );
    const graph = registry.getDotDependencyGraph();
    fs.writeFileSync(addonsDepGraphPath, new Buffer.from(graph));
  }

  config.externals =
    target === 'node'
      ? [
          nodeExternals({
            allowlist: [
              dev ? 'webpack/hot/poll?300' : null,
              /\.(eot|woff|woff2|ttf|otf)$/,
              /\.(svg|png|jpg|jpeg|gif|ico)$/,
              /\.(mp4|mp3|ogg|swf|webp)$/,
              /\.(css|scss|sass|sss|less)$/,
              // Add support for addons to include externals (ie. node_modules npm published packages)
              ...addonsAsExternals,
              /^@plone\/volto/,
            ].filter(Boolean),
          }),
        ]
      : [];

  if (config.devServer) {
    config.devServer.watchOptions.ignored = /node_modules\/(?!@plone\/volto)/;
  }

  return config;
};

const addonExtenders = registry.getAddonExtenders().map((m) => require(m));

const defaultPlugins = [
  { object: require('./webpack-plugins/webpack-less-plugin')({ registry }) },
  { object: require('./webpack-plugins/webpack-svg-plugin') },
  { object: require('./webpack-plugins/webpack-bundle-analyze-plugin') },
  { object: require('./jest-extender-plugin') },
];

const plugins = addonExtenders.reduce(
  (acc, extender) => extender.plugins(acc),
  defaultPlugins,
);

module.exports = {
  plugins,
  modifyJestConfig: ({ jestConfig }) => {
    jestConfig.testEnvironment = 'jsdom';
    return jestConfig;
  },
  modifyWebpackConfig: ({
    env: { target, dev },
    webpackConfig,
    webpackObject,
    options,
  }) => {
    const defaultConfig = defaultModify({
      env: { target, dev },
      webpackConfig,
      webpackObject,
      options,
    });

    const res = addonExtenders.reduce(
      (acc, extender) => extender.modify(acc, { target, dev }, webpackConfig),
      defaultConfig,
    );
    return res;
  },
  options: {
    enableReactRefresh: true,
  },
};
